/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.xpath;

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.dom4j.InvalidXPathException;
import org.dom4j.Node;
import org.dom4j.NodeFilter;
import org.dom4j.XPathException;

import org.jaxen.FunctionContext;
import org.jaxen.JaxenException;
import org.jaxen.NamespaceContext;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.VariableContext;
import org.jaxen.XPath;
import org.jaxen.dom4j.Dom4jXPath;

/**
 * <p> Default implementation of {@link org.dom4j.XPath}which uses the <a
 * href="http://jaxen.org">Jaxen </a> project. </p>
 * 
 * @author bob mcwhirter
 * @author <a href="mailto:jstrachan@apache.org">James Strachan </a>
 */
public class DefaultXPath implements org.dom4j.XPath, NodeFilter, Serializable
{
   private String text;

   private XPath xpath;

   private NamespaceContext namespaceContext;

   /**
    * Construct an XPath
    * 
    * @param text DOCUMENT ME!
    * 
    * @throws InvalidXPathException DOCUMENT ME!
    */
   public DefaultXPath(String text) throws InvalidXPathException
   {
      this.text = text;
      this.xpath = parse(text);
   }

   public String toString()
   {
      return "[XPath: " + xpath + "]";
   }

   // XPath interface

   /**
    * Retrieve the textual XPath string used to initialize this Object
    * 
    * @return The XPath string
    */
   public String getText()
   {
      return text;
   }

   public FunctionContext getFunctionContext()
   {
      return xpath.getFunctionContext();
   }

   public void setFunctionContext(FunctionContext functionContext)
   {
      xpath.setFunctionContext(functionContext);
   }

   public NamespaceContext getNamespaceContext()
   {
      return namespaceContext;
   }

   public void setNamespaceURIs(Map map)
   {
      setNamespaceContext(new SimpleNamespaceContext(map));
   }

   public void setNamespaceContext(NamespaceContext namespaceContext)
   {
      this.namespaceContext = namespaceContext;
      xpath.setNamespaceContext(namespaceContext);
   }

   public VariableContext getVariableContext()
   {
      return xpath.getVariableContext();
   }

   public void setVariableContext(VariableContext variableContext)
   {
      xpath.setVariableContext(variableContext);
   }

   public Object evaluate(Object context)
   {
      try
      {
         setNSContext(context);

         List answer = xpath.selectNodes(context);

         if((answer != null) && (answer.size() == 1))
         {
            return answer.get(0);
         }

         return answer;
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return null;
      }
   }

   public Object selectObject(Object context)
   {
      return evaluate(context);
   }

   public List selectNodes(Object context)
   {
      try
      {
         setNSContext(context);

         return xpath.selectNodes(context);
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return Collections.EMPTY_LIST;
      }
   }

   public List selectNodes(Object context, org.dom4j.XPath sortXPath)
   {
      List answer = selectNodes(context);
      sortXPath.sort(answer);

      return answer;
   }

   public List selectNodes(Object context, org.dom4j.XPath sortXPath,
      boolean distinct)
   {
      List answer = selectNodes(context);
      sortXPath.sort(answer, distinct);

      return answer;
   }

   public Node selectSingleNode(Object context)
   {
      try
      {
         setNSContext(context);

         Object answer = xpath.selectSingleNode(context);

         if(answer instanceof Node)
         {
            return (Node) answer;
         }

         if(answer == null)
         {
            return null;
         }

         throw new XPathException("The result of the XPath expression is "
            + "not a Node. It was: " + answer + " of type: "
            + answer.getClass().getName());
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return null;
      }
   }

   public String valueOf(Object context)
   {
      try
      {
         setNSContext(context);

         return xpath.stringValueOf(context);
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return "";
      }
   }

   public Number numberValueOf(Object context)
   {
      try
      {
         setNSContext(context);

         return xpath.numberValueOf(context);
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return null;
      }
   }

   public boolean booleanValueOf(Object context)
   {
      try
      {
         setNSContext(context);

         return xpath.booleanValueOf(context);
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return false;
      }
   }

   /**
    * <p> <code>sort</code> sorts the given List of Nodes using this XPath
    * expression as a {@link Comparator}. </p>
    * 
    * @param list is the list of Nodes to sort
    */
   public void sort(List list)
   {
      sort(list, false);
   }

   /**
    * <p> <code>sort</code> sorts the given List of Nodes using this XPath
    * expression as a {@link Comparator}and optionally removing duplicates.
    * </p>
    * 
    * @param list is the list of Nodes to sort
    * @param distinct if true then duplicate values (using the sortXPath for
    *           comparisions) will be removed from the List
    */
   public void sort(List list, boolean distinct)
   {
      if((list != null) && !list.isEmpty())
      {
         int size = list.size();
         HashMap sortValues = new HashMap(size);

         for(int i = 0; i < size; i++)
         {
            Object object = list.get(i);

            if(object instanceof Node)
            {
               Node node = (Node) object;
               Object expression = getCompareValue(node);
               sortValues.put(node, expression);
            }
         }

         sort(list, sortValues);

         if(distinct)
         {
            removeDuplicates(list, sortValues);
         }
      }
   }

   public boolean matches(Node node)
   {
      try
      {
         setNSContext(node);

         List answer = xpath.selectNodes(node);

         if((answer != null) && (answer.size() > 0))
         {
            Object item = answer.get(0);

            if(item instanceof Boolean)
            {
               return ((Boolean) item).booleanValue();
            }

            return answer.contains(node);
         }

         return false;
      }
      catch(JaxenException e)
      {
         handleJaxenException(e);

         return false;
      }
   }

   /**
    * Sorts the list based on the sortValues for each node
    * 
    * @param list DOCUMENT ME!
    * @param sortValues DOCUMENT ME!
    */
   protected void sort(List list, final Map sortValues)
   {
      Collections.sort(list, new Comparator()
         {
            public int compare(Object o1, Object o2)
            {
               o1 = sortValues.get(o1);
               o2 = sortValues.get(o2);

               if(o1 == o2)
               {
                  return 0;
               }
               else if(o1 instanceof Comparable)
               {
                  Comparable c1 = (Comparable) o1;

                  return c1.compareTo(o2);
               }
               else if(o1 == null)
               {
                  return 1;
               }
               else if(o2 == null)
               {
                  return -1;
               }
               else
               {
                  return o1.equals(o2) ? 0 : (-1);
               }
            }
         });
   }

   // Implementation methods

   /**
    * Removes items from the list which have duplicate values
    * 
    * @param list DOCUMENT ME!
    * @param sortValues DOCUMENT ME!
    */
   protected void removeDuplicates(List list, Map sortValues)
   {
      // remove distinct
      HashSet distinctValues = new HashSet();

      for(Iterator iter = list.iterator(); iter.hasNext();)
      {
         Object node = iter.next();
         Object value = sortValues.get(node);

         if(distinctValues.contains(value))
         {
            iter.remove();
         }
         else
         {
            distinctValues.add(value);
         }
      }
   }

   /**
    * DOCUMENT ME!
    * 
    * @param node DOCUMENT ME!
    * 
    * @return the node expression used for sorting comparisons
    */
   protected Object getCompareValue(Node node)
   {
      return valueOf(node);
   }

   protected static XPath parse(String text)
   {
      try
      {
         return new Dom4jXPath(text);
      }
      catch(JaxenException e)
      {
         throw new InvalidXPathException(text, e.getMessage());
      }
      catch(Throwable t)
      {
         throw new InvalidXPathException(text, t);
      }
   }

   protected void setNSContext(Object context)
   {
      if(namespaceContext == null)
      {
         xpath.setNamespaceContext(DefaultNamespaceContext.create(context));
      }
   }

   protected void handleJaxenException(JaxenException exception)
      throws XPathException
   {
      throw new XPathException(text, exception);
   }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
